﻿//-----------------------------------------------------------------------------------------------------------
// WBFSSync Project by Omega Frost 
// http://wbfssync.codeplex.com/
//
// WBFSSync is Licensed under the terms of the 
// Microsoft Reciprocal License (Ms-RL)
//-----------------------------------------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using WBFSe3.Wbfs;
using System.Threading;
using WBFSe3.Streams;
using WBFSe3.Formats;
using WBFSe3.Interfaces;
using System.Security;

namespace WBFSe3.IO
{
    //-------------------------------------------------------------------------------------------------------
    //
    //-------------------------------------------------------------------------------------------------------
    public delegate void FileCreatedDelegate(WbfsFileInfo sender);
    public delegate void FileDeletedDelegate(WbfsFileInfo sender);
    public delegate void FileUpdatedDelegate(WbfsFileInfo sender);


    //-------------------------------------------------------------------------------------------------------
    //
    //-------------------------------------------------------------------------------------------------------
    public class WbfsFileInfo : FileSystemInfo, IWbfsGameInfo
    {
        //---------------------------------------------------------------------------------------------------
        //
        //---------------------------------------------------------------------------------------------------
        private static int UidCounter = 0;


        //---------------------------------------------------------------------------------------------------
        // Indexing specific
        //---------------------------------------------------------------------------------------------------
        internal int index = -1;


        //---------------------------------------------------------------------------------------------------
        //
        //---------------------------------------------------------------------------------------------------
        private int uid;
        private bool buildFailed;
        private bool notAGame;
        private int lastError;

        protected String name;
        protected String code;
        protected WbfsRegion region;
        protected Boolean hasUpdate;
        protected Boolean hasChannel;

        protected bool doubleLayer;
        protected long length;
        protected long size;

        protected WbfsUsage usage;
        protected int usedSectors;
        protected int sectorSize;

        protected WbfsPath files;
        protected WbfsFormat format;

        protected WbfsFormatCreator creator;
        protected WbfsFormatReader reader;

        protected WbfsDriveInfo drive;
        
        protected DateTime lastUpdateUtc;


        //---------------------------------------------------------------------------------------------------
        //
        //---------------------------------------------------------------------------------------------------
        public event FileCreatedDelegate FileCreated;
        public event FileDeletedDelegate FileDeleted;
        public event FileUpdatedDelegate FileUpdated;
        

        //---------------------------------------------------------------------------------------------------
        //
        //---------------------------------------------------------------------------------------------------
        public int Uid { get { return this.uid; } }

        public override string Name { get { return this.name; } }
        public string Code { get { return this.code; } }
        public WbfsRegion Region { get { return this.region; } }

        public Boolean HasUpdate { get { return this.hasUpdate; } }
        public Boolean HasChannel { get { return this.hasChannel; } }

        public WbfsPath FileNames { get { return this.files; } }
        public WbfsFormat Format { get { return this.format; } }

        public WbfsDriveInfo Drive { get { return this.drive; } }

        public bool IsTemporary
        {
            get { return this.creator != null; }
        }

        public bool IsReadOnly
        {
            get { return GetReadOnly(); }
            set { SetReadOnly(value); }
        }

        public override bool Exists
        {
            get { return GetFileState() != WbfsFileState.Deleted; }
        }

        public bool DoubleLayer { get { return this.doubleLayer; } }
        public long Length { get { return this.length; } }
        public long Size { get { return this.size; } }

        public WbfsUsage WiiSectorUsage { get { return this.usage; } }
        public int SectorSize { get { return this.sectorSize; } }

        public WbfsFileState State { get { return GetFileState(); } }
        public int LastError { get { return this.lastError; } }

        public new DateTime CreationTime { get { return GetLastCreation(false); } }
        public new DateTime CreationTimeUtc { get { return GetLastCreation(true); } }

        public new DateTime LastAccessTime { get { return GetLastAccess(false); } }
        public new DateTime LastAccessTimeUtc { get { return GetLastAccess(true); } }

        public new DateTime LastWriteTime { get { return GetLastWrite(false); } }
        public new DateTime LastWriteTimeUtc { get { return GetLastWrite(true); } }

        public DateTime LastUpdateTime { get { return this.lastUpdateUtc.ToLocalTime(); } }
        public DateTime LastUpdateTimeUtc { get { return this.lastUpdateUtc; } }


        //---------------------------------------------------------------------------------------------------
        //
        //---------------------------------------------------------------------------------------------------
        internal WbfsFileInfo(WbfsDriveInfo drive, WbfsPath path, WbfsFormatReader reader, WbfsFormat format)
        {
            // Setup basics

            this.uid = Interlocked.Increment(ref UidCounter);

            this.creator = null;
            this.reader = reader;

            this.drive = drive;
            this.files = reader.Path;
            this.format = format;

            if (reader.SectorSize > 0)
            {
                this.sectorSize = reader.SectorSize;
            }
            else
            {
                this.sectorSize = WbfsConstants.WiiSectorSize;
            }

            this.size = reader.FileSize;
            this.length = reader.FileLength;

            // Build the disc info

            ParseDisc(true);

            if (!this.notAGame || !WbfsSearch.GameOnly)
            {
                // Index it
                WbfsSearch.AddFile(this);
            }
        }


        //---------------------------------------------------------------------------------------------------
        //
        //---------------------------------------------------------------------------------------------------
        internal WbfsFileInfo(WbfsDriveInfo drive, WbfsFormatCreator creator, WbfsFormat format)
        {
            // Setup basics
            this.uid = Interlocked.Increment(ref UidCounter);

            this.creator = creator;
            this.reader = null;

            this.drive = drive;
            this.files = creator.Path;
            this.format = format;

            this.size = creator.EstimateFileSize();

            if (creator.SectorSize > 0)
            {
                this.sectorSize = creator.SectorSize;
            }
            else
            {
                this.sectorSize = WbfsConstants.WiiSectorSize;
            }

            // Index it
            WbfsSearch.AddFile(this);
        }


        //---------------------------------------------------------------------------------------------------
        //
        //---------------------------------------------------------------------------------------------------
        public WbfsError SetName(String name)
        {
            byte[] bname = null;
            WbfsStream stream = null;
            WbfsError error;

            if (name.Length > WbfsConstants.WiiNameLen)
                name = name.Remove(WbfsConstants.WiiNameLen);

            bname = new byte[WbfsConstants.WiiNameLen];
            Encoding.Default.GetBytes(name).CopyTo(bname, 0);

            try
            {
                error = Open(FileAccess.Write, FileShare.Read, out stream);
                if (error != WbfsError.Ok) goto END;

                if (!stream.CanSeek)
                {
                    error = WbfsError.IOSeekError;
                }
                else if (!stream.CanWrite)
                {
                    error = WbfsError.IOWriteError;
                }
                else
                {
                    stream.Position = WbfsConstants.WiiNamePos;
                    stream.Write(bname, 0, WbfsConstants.WiiNameLen);

                    this.name = name;
                    error = WbfsError.Ok;
                }
            }
            catch (WbfsIOException exception)
            {
                error = (WbfsError)exception.NativeError;
            }
            catch (Exception)
            {
                error = WbfsError.Fail;
            }
            finally
            {
                if (stream != null)
                    stream.Close();
            }

        END:
            this.lastError = (int)error;
            return error;
        }


        //---------------------------------------------------------------------------------------------------
        //
        //---------------------------------------------------------------------------------------------------
        public WbfsError SetCode(String code)
        {
            byte[] bcode = null;
            WbfsStream stream = null;
            WbfsError error;

            if (code.Length > WbfsConstants.WiiCodeLen)
                code = code.Remove(WbfsConstants.WiiCodeLen);

            bcode = new byte[WbfsConstants.WiiCodeLen];
            Encoding.Default.GetBytes(code).CopyTo(bcode, 0);

            error = Open(FileAccess.Write, FileShare.Read, out stream);
            if (error != WbfsError.Ok) goto END;

            try
            {
                stream.Position = WbfsConstants.WiiCodePos;
                stream.Write(bcode, 0, WbfsConstants.WiiCodeLen);

                this.code = code;
                error = WbfsError.Ok;
            }
            catch (WbfsIOException exception)
            {
                error = (WbfsError)exception.NativeError;
            }
            catch (Exception)
            {
                error = WbfsError.Fail;
            }
            finally
            {
                if (stream != null)
                    stream.Close();
            }

        END:
            this.lastError = (int)error;
            return error;
        }


        //---------------------------------------------------------------------------------------------------
        //
        //---------------------------------------------------------------------------------------------------
        public WbfsError PatchRegion(WbfsRegion region)
        {
            WbfsError error;
            WbfsStream stream = null;
            Byte[] regiondata = null;

            switch (region)
            {
                case WbfsRegion.NTSCU: regiondata = WbfsConstants.DefaultUsaRegionCode; break;
                case WbfsRegion.NTSCJ: regiondata = WbfsConstants.DefaultJapRegionCode; break;
                case WbfsRegion.PAL: regiondata = WbfsConstants.DefaultPalRegionCode; break;
                default: return WbfsError.DiscInvalidRegion;
            }

            error = Open(FileAccess.Write, FileShare.Read, out stream);
            if (error != WbfsError.Ok) goto END;

            try
            {
                error = WbfsError.Ok;
            }
            catch (WbfsIOException exception)
            {
                error = (WbfsError)exception.NativeError;
            }
            catch (Exception)
            {
                error = WbfsError.Fail;
            }
            finally
            {
                if (stream != null)
                    stream.Close();
            }

        END:
            this.lastError = (int)error;
            return error;
        }


        //---------------------------------------------------------------------------------------------------
        //
        //---------------------------------------------------------------------------------------------------
        private bool GetReadOnly()
        {
            if (this.drive.IsWbfs)
                return this.drive.IsReadOnly;

            for (int i = 0; i < this.files.Count; i++)
            {
                if ((File.GetAttributes(this.files[i]) | FileAttributes.ReadOnly) != 0)
                    return true;
            }

            return false;
        }


        //---------------------------------------------------------------------------------------------------
        //
        //---------------------------------------------------------------------------------------------------
        private void SetReadOnly(bool value)
        {
            if (this.drive.IsWbfs)
                return;

            for (int i = 0; i < this.files.Count; i++)
            {
                FileAttributes att = File.GetAttributes(this.files[i]);
                att &= ~FileAttributes.ReadOnly;
                File.SetAttributes(this.files[i], att);
            }
        }


        //---------------------------------------------------------------------------------------------------
        //
        //---------------------------------------------------------------------------------------------------
        private WbfsFileState GetFileState()
        {
            if (this.buildFailed)
            {
                if (this.notAGame) return WbfsFileState.NotAGame;
                else return WbfsFileState.Error;
            }

            if (this.creator != null)
                return WbfsFileState.Temporary;

            // All discs are indexed
            if (this.index < 0)
                return WbfsFileState.Deleted;

            if (drive.IsWbfs)
            {
                if ((drive as WbfsDevice).GetDiscIndex(this.files.Files[0]) < 0)
                    return WbfsFileState.Deleted;
            }
            else
            {
                int deleted = 0;
                DateTime current, newest = new DateTime();
                for (int i = 0; i < this.files.Count; i++)
                {
                    string file = this.files[i];
                    if (!File.Exists(file)) deleted++;
                    else
                    {
                        current = File.GetLastWriteTimeUtc(file);
                        if (current > newest) newest = current;
                    }
                }

                if (deleted == this.files.Count)
                    return WbfsFileState.Deleted;
                else if (deleted > 0)
                    return WbfsFileState.Currupted;

                if (newest > this.lastUpdateUtc)
                    return WbfsFileState.NotUpdated;
            }

            return WbfsFileState.Ok;
        }


        //---------------------------------------------------------------------------------------------------
        //
        //---------------------------------------------------------------------------------------------------
        private DateTime GetLastWrite(bool utc)
        {
            if (this.drive.IsWbfs)
                return new DateTime();

            DateTime newest = new DateTime();
            DateTime current;

            for (int i = 0; i < this.files.Count; i++)
            {
                if(utc) current = File.GetLastWriteTimeUtc(this.files[i]);
                else current = File.GetLastWriteTime(this.files[i]);

                if (current > newest)
                    newest = current;
            }

            return newest;
        }


        //---------------------------------------------------------------------------------------------------
        //
        //---------------------------------------------------------------------------------------------------
        private DateTime GetLastAccess(bool utc)
        {
            if (this.drive.IsWbfs)
                return new DateTime();

            DateTime newest = new DateTime();
            DateTime current;

            for (int i = 0; i < this.files.Count; i++)
            {
                if (utc) current = File.GetLastAccessTimeUtc(this.files[i]);
                else current = File.GetLastAccessTime(this.files[i]);

                if (current > newest)
                    newest = current;
            }

            return newest;
        }


        //---------------------------------------------------------------------------------------------------
        //
        //---------------------------------------------------------------------------------------------------
        private DateTime GetLastCreation(bool utc)
        {
            if (this.drive.IsWbfs)
                return new DateTime();

            DateTime newest = new DateTime();
            DateTime current;

            for (int i = 0; i < this.files.Count; i++)
            {
                if (utc) current = File.GetCreationTimeUtc(this.files[i]);
                else current = File.GetCreationTime(this.files[i]);

                if (current > newest)
                    newest = current;
            }

            return newest;
        }
        
        
        //---------------------------------------------------------------------------------------------------
        //
        //---------------------------------------------------------------------------------------------------
        public WbfsError Update()
        {
            if (GetFileState() == WbfsFileState.Deleted)
            {
                WbfsSearch.RemoveFile(this);
                return WbfsError.Fail;
            }
            else
            {
                WbfsError e = ParseDisc(false);
                OnFileUpdated();
                return e;
            }
        }
        
        
        //---------------------------------------------------------------------------------------------------
        //
        //---------------------------------------------------------------------------------------------------
        public WbfsError ParseDisc(bool buildAll)
        { 
            if (this.IsTemporary)
                return WbfsError.Ok;

            this.lastUpdateUtc = DateTime.UtcNow;

            WbfsBuilder builder = null;
            WbfsStream stream = null;
            WbfsError error;

            try
            {
                error = Open(FileAccess.Read, FileShare.Read, out stream);

                if (error != WbfsError.Ok)
                {
                    this.buildFailed = true;
                    this.lastError = (int)error;
                    return error;
                }

                builder = new WbfsBuilder(stream);
                if ((error = builder.Build(buildAll)) != WbfsError.Ok)
                {
                    //The builder will log errors...
                    this.buildFailed = true;

                    if (error == WbfsError.DiscMagic1Incorrect)
                        this.notAGame = true;

                    this.lastError = (int)error;
                    return error;
                }
            }
            catch
            {
                this.buildFailed = true;
                this.lastError = (int)WbfsError.Fail;
                return WbfsError.Fail;
            }
            finally
            {
                if (stream != null)
                    stream.Close();
            }

            lock (this)
            {
                this.name = builder.Name;
                this.code = builder.Code;
                this.region = builder.CodeRegion;

                if(buildAll)
                {
                    this.hasUpdate = builder.HasUpdate;
                    this.hasChannel = builder.HasChannel;

                    this.usedSectors = builder.UsedWiiSectors;
                    this.usage = new WbfsUsage(builder.WiiSectorUsage);

                    if (this.usage.GetLastOne() >= (WbfsConstants.WiiSectorsPerDisc / 2))
                        this.doubleLayer = true;
                }

                this.buildFailed = false;
                this.notAGame = false;
            }

            return WbfsError.Ok;
        }


        //---------------------------------------------------------------------------------------------------
        //
        //---------------------------------------------------------------------------------------------------
        public WbfsError Open(out WbfsStream stream)
        {
            return Open(FileAccess.ReadWrite, FileShare.None, out stream);
        }


        //---------------------------------------------------------------------------------------------------
        //
        //---------------------------------------------------------------------------------------------------
        public WbfsError Open(FileAccess access, out WbfsStream stream)
        {
            return Open(access, FileShare.None, out stream);
        }


        //---------------------------------------------------------------------------------------------------
        //
        //---------------------------------------------------------------------------------------------------
        public WbfsError Open(FileAccess access, FileShare share, out WbfsStream stream)
        {
            WbfsError error;

            try
            {
                if (!this.IsTemporary)
                    return this.reader.GetStream(access, share, out stream);
                else
                    return creator.BeginCreate(out stream);
            }
            catch (FileNotFoundException)
            {
                error = WbfsError.IOFileNotFound;
            }
            catch (DirectoryNotFoundException)
            {
                error = WbfsError.IODirNotFound;
            }
            catch (SecurityException)
            {
                error = WbfsError.IOSecurityError;
            }
            catch (SharingViolationException)
            {
                error = WbfsError.IOSharingViolation;
            }
            catch (UnauthorizedAccessException)
            {
                error = WbfsError.IOAccessDenied;
            }
            catch (WbfsIOException exception)
            {
                error = (WbfsError)exception.NativeError;
            }
            catch (IOException)
            {
                error = WbfsError.IOError;
            }
            catch (Exception)
            {
                error = WbfsError.Fail;
            }

            stream = null;
            return error;
        }


        //---------------------------------------------------------------------------------------------------
        //
        //---------------------------------------------------------------------------------------------------
        public WbfsError OpenRead(out WbfsStream stream)
        {
            return Open(FileAccess.Read, out stream);
        }


        //---------------------------------------------------------------------------------------------------
        //
        //---------------------------------------------------------------------------------------------------
        public WbfsError OpenWrite(out WbfsStream stream)
        {
            return Open(FileAccess.Write, out stream);
        }


        //---------------------------------------------------------------------------------------------------
        //
        //---------------------------------------------------------------------------------------------------
        public WbfsError Consolidate(bool succeeded)
        {
            if (this.creator == null) 
                return WbfsError.Ok;

            if (!succeeded)
            {
                WbfsSearch.RemoveFile(this);
                return this.creator.EndCreate(false);
            }

            try
            {
                WbfsError error;
                WbfsPath path;
                String file = null;

                path = new WbfsPath(this.creator.Path);
                foreach (String f in path.Files)
                {
                    if (f.EndsWith(this.format.Extension))
                    {
                        file = f;
                        break;
                    }
                }

                error = this.creator.EndCreate(true);
                if (error != WbfsError.Ok) return error;
                
                this.creator = null;
                this.reader = this.format.InvokeReader();

                error = this.reader.Initialize(this.drive, path, file);
                if (error != WbfsError.Ok) return error;

                // Setup basics

                this.files = reader.Path;

                if (reader.SectorSize > 0)
                {
                    this.sectorSize = reader.SectorSize;
                }
                else
                {
                    this.sectorSize = WbfsConstants.WiiSectorSize;
                }

                this.size = reader.FileSize;
                this.length = reader.FileLength;

                // Build the disc info

                return ParseDisc(true);
            }
            catch (FileNotFoundException)
            {
                return WbfsError.IOFileNotFound;
            }
            catch (DirectoryNotFoundException)
            {
                return WbfsError.IODirNotFound;
            }
            catch (SecurityException)
            {
                return WbfsError.IOSecurityError;
            }
            catch (SharingViolationException)
            {
                return WbfsError.IOSharingViolation;
            }
            catch (UnauthorizedAccessException)
            {
                return WbfsError.IOAccessDenied;
            }
            catch (WbfsIOException exception)
            {
                return (WbfsError)exception.NativeError;
            }
            catch (IOException)
            {
                return WbfsError.IOError;
            }
            catch (Exception)
            {
                return WbfsError.Fail;
            }
        }


        //---------------------------------------------------------------------------------------------------
        //
        //---------------------------------------------------------------------------------------------------
        public override void Delete()
        {
            //Not yet...
        }


        //---------------------------------------------------------------------------------------------------
        //
        //---------------------------------------------------------------------------------------------------
        protected void OnFileCreated()
        {
            if (this.FileCreated != null)
                this.FileCreated(this);
        }


        //---------------------------------------------------------------------------------------------------
        //
        //---------------------------------------------------------------------------------------------------
        protected void OnFileDeleted()
        {
            if (this.FileDeleted != null)
                this.FileDeleted(this);
        }


        //---------------------------------------------------------------------------------------------------
        //
        //---------------------------------------------------------------------------------------------------
        protected void OnFileUpdated()
        {
            if (this.FileUpdated != null)
                this.FileUpdated(this);
        }


        //---------------------------------------------------------------------------------------------------
        //
        //---------------------------------------------------------------------------------------------------
        public override string ToString()
        {
            if (String.IsNullOrEmpty(this.name))
                return this.files[0];
            else
                return this.name;
        }
    }
}
